package org.javanum.number;

import java.math.BigDecimal;
import java.math.MathContext;

import static org.javanum.number.BigMath.*;

/**
 * Copyright 2010 Scott Fines
 * <p/>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * <p/>
 *
 * <p>Represents a Utility class with implementations of
 * common functions operating on BigDecimal numbers, using
 * algorithms based on Taylor expansions.</p>
 *
 * Author: Scott Fines
 * Date: Jan 24, 2010
 */
public class BigTaylor {
    private BigTaylor(){}

    /**
     * <p>computes the sin of a given BigDecimal.</p>
     *
     * <p>In this implementation,If the input is
     * equal to 0+-2*pi*k, for k an integer, then this method
     * returns BigDecimal.ZERO. If the input is equal to pi/2+-pi, then
     * this method returns BigDecimal.ONE. Otherwise, this implementation
     * uses the Taylor Series of sin taken to enough decimal places
     * to ensure that the Taylor remainder is bounded above by the
     * specified precision. In some situations, this may, but is
     * not required to, return a result which has higher precision than
     * specified, but it will never return lower.</p>
     *
     * @param input the argument to the sin() function
     * @param precision the precision of the result
     * @return the result of computing sin(input)
     */
    public static BigDecimal sin(BigDecimal input, MathContext precision){
        int prec = precision.getPrecision();

        /*check to ensure that the passed in value is not
        one of the obvious ones, like 0,pi/2,pi,3pi/2, or 2pi
        */
        BigDecimal pi = BigMath.pi(precision);
        BigDecimal twoPi = TWO.multiply(pi);
        BigDecimal piOver2 = pi.divide(TWO);

        BigDecimal x = input.abs();
        BigDecimal y = x.remainder(twoPi);

        //check zero
        if(equalToWithinPrecision(
                y,BigDecimal.ZERO,prec))
            return BigDecimal.ZERO;
        //check pi
        if(equalToWithinPrecision(y,pi,prec))
                return BigDecimal.ZERO;
        //check 2pi
//        else if(equalToWithinPrecision(y,twoPi,prec))
//                return BigDecimal.ZERO;
        //check pi/2
        else if(equalToWithinPrecision(y,piOver2,prec))
                return BigDecimal.ONE;
        //check 3pi/2
        else if(equalToWithinPrecision(y,piOver2.
                                multiply(new BigDecimal("3.0")),prec))
                return BigDecimal.ONE.negate();

        /*
        Use a Taylor series to determine the approximation
        taken to the specified number of decimal places
         */
        BigDecimal t = x.remainder(pi);
        BigDecimal value = t.subtract(piOver2);
        int m=1;
        BigDecimal fact = BigDecimal.ONE;
        BigDecimal remainder = value.abs().pow(m).divide(fact);
        BigDecimal result = BigDecimal.ONE;
        BigDecimal tol = BigDecimal.ONE.movePointLeft(prec);
        BigDecimal currentPow = BigDecimal.ONE;
        boolean evenTerm=false;
        MathContext dividePrecision = new MathContext(2*prec);
        while(remainder.compareTo(tol)>0){
            fact = fact.multiply(new BigDecimal(2*m-1)).
                    multiply(new BigDecimal(2*m));

            currentPow = currentPow.multiply(value.pow(2));
            if(!evenTerm){
                result = result.subtract(currentPow.
                        divide(fact,dividePrecision));
            }else{
                result = result.add(currentPow.
                        divide(fact,dividePrecision));
            }

            remainder = remainder.multiply(value.abs()).
                    divide(new BigDecimal(m+1),dividePrecision);
            m++;
            evenTerm = !evenTerm;
        }
        if(y.compareTo(pi)>0){
            result = result.negate();
        }

        if(input.compareTo(BigDecimal.ZERO)<0){
            return result.negate();
        }else{
            return result;
        }
    }

    public static BigDecimal cos(BigDecimal x,MathContext precision){
        return sin(x.add(pi(precision).divide(TWO)),precision);
    }

    public static BigDecimal tan(BigDecimal input,MathContext precision){
        BigDecimal PI = pi(precision);
        BigDecimal piOver2 = PI.divide(TWO);
        BigDecimal threePiOver2 = new BigDecimal("3.0").multiply(piOver2);
        BigDecimal y = input.abs().remainder(PI);
        if(equalToWithinPrecision(y,piOver2,
                                    precision.getPrecision()))
            throw new ArithmeticException("Tangent of pi/2 is undefined " +
                    "for type BigDecimal");
        if(equalToWithinPrecision(y,threePiOver2,
                                    precision.getPrecision()))
            throw new ArithmeticException("Tangent of 3pi/2 is undefined " +
                    "for type BigDecimal");

        BigDecimal piOver4 = piOver2.divide(TWO);
        if(equalToWithinPrecision(y,piOver4,precision.getPrecision()))
            return BigDecimal.ONE;

        BigDecimal threePiOver4 = new BigDecimal("3.0").multiply(piOver4);
        if(equalToWithinPrecision(y,threePiOver4,precision.getPrecision()))
            return BigDecimal.ONE.negate();

        return sin(input,precision).divide(cos(input,precision),precision);
    }

    public static BigDecimal exp(BigDecimal x,MathContext precision){
        if(BigMath.equalToWithinPrecision(x,
                            BigDecimal.ZERO,precision.getPrecision()))
            return BigDecimal.ONE;

        BigDecimal mult = BigDecimal.ONE;
        BigDecimal fact = BigDecimal.ONE;
        BigDecimal TOL = BigDecimal.ONE.
                                movePointLeft(precision.getPrecision());
        BigDecimal exp = BigDecimal.ONE;
        BigDecimal oldExp;
        BigDecimal count = BigDecimal.ZERO;
        do{
            count = count.add(BigDecimal.ONE);
            oldExp = exp;
            mult = mult.multiply(x);
            fact = fact.multiply(count);
            exp = exp.add(mult.divide(fact,precision));
        }while(exp.subtract(oldExp).abs().compareTo(TOL)>0);
        return exp;
    }

    public static BigDecimal ln(BigDecimal x,MathContext precision){
        if(BigDecimal.ZERO.compareTo(x)>=0)
            throw new ArithmeticException(
                    "Cannot compute the natural log of a negative number");

        BigDecimal p1 = new BigDecimal(Math.log(x.doubleValue()));
        BigDecimal p0;
        BigDecimal tol = BigDecimal.ONE.movePointLeft(precision.getPrecision());
        MathContext prec =
                new MathContext(
                        2*precision.getPrecision(),
                        precision.getRoundingMode());
        do{
            p0=p1;
            p1 = p0.subtract(BigDecimal.ONE.subtract((x.divide(exp(p0,prec),prec))));
        }while(p1.subtract(p0).abs().compareTo(tol)>=0);

        return p1;
    }

    public static BigDecimal computeLn2Series(MathContext precision){
        MathContext prec = new MathContext(precision.getPrecision()+1,
                                            precision.getRoundingMode());
        BigDecimal oldLn,ln = oneHalf;
        BigDecimal k = BigDecimal.ONE;
        BigDecimal twoPow = TWO;
        BigDecimal tol = BigDecimal.ONE.movePointLeft(prec.getPrecision());
        do{
            oldLn=ln;
            k = k.add(BigDecimal.ONE);
            twoPow = twoPow.multiply(TWO);
            BigDecimal bot = twoPow.multiply(k);
            ln = ln.add(BigDecimal.ONE.divide(bot,prec));
        }while(ln.subtract(oldLn).abs().compareTo(tol)>0);

        return ln.round(precision);
    }

    public static BigDecimal e(MathContext precision){
        MathContext context =
                new MathContext(
                        precision.getPrecision()+2,precision.getRoundingMode());
        BigDecimal oldE,e = BigMath.TWO;
        BigDecimal tol = BigDecimal.ONE.movePointLeft(context.getPrecision());
        BigDecimal fact = BigDecimal.ONE;
        BigDecimal sum = BigDecimal.ONE;
        do{
            oldE = e;
            sum = sum.add(BigDecimal.ONE);
            fact = fact.multiply(sum);

            e = BigDecimal.ONE.divide(fact,context).add(e);
        }while(e.subtract(oldE).abs().compareTo(tol)>=0);

        return e.round(context);
    }

}
